Skip to content

feat: implement large-room post restriction in message-gatekeeper#162

Merged
mliu33 merged 21 commits intomainfrom
claude/validate-message-sending-5HTd9
May 8, 2026
Merged

feat: implement large-room post restriction in message-gatekeeper#162
mliu33 merged 21 commits intomainfrom
claude/validate-message-sending-5HTd9

Conversation

@general-lex
Copy link
Copy Markdown
Collaborator

@general-lex general-lex commented May 8, 2026

Summary

Implements a large-room post restriction that prevents non-owner/non-admin members from sending top-level messages in rooms exceeding a configurable member threshold (default 500). Thread replies and bot accounts are exempt. The check is added to message-gatekeeper as an admission gate before publishing to MESSAGES_CANONICAL.

Key Changes

Model & Shared Utilities

  • Add backward-compatible Code field to model.ErrorResponse with omitempty tag to preserve wire format for existing callers
  • Add RoleAdmin constant to pkg/model/subscription.go for bypass eligibility (assignment wiring owned by separate team)
  • Add MarshalErrorWithCode helper to pkg/natsutil for typed error replies

Message Gatekeeper

  • Add GetRoom method to Store interface and implement in MongoStore to fetch room member counts
  • Add LargeRoomThreshold configuration (env: LARGE_ROOM_THRESHOLD, default: 500)
  • Implement canBypassLargeRoomCap predicate that exempts owners, admins, and bot accounts (detected via regex pattern \.bot$|^p_)
  • Add codedError type and errLargeRoomPostRestricted sentinel for validation errors with machine-readable codes
  • Add marshalErrorReply dispatch helper to route errors through appropriate marshaling (coded vs. uncoded)
  • Insert large-room check in processMessage after subscription lookup but before quote resolution, skipping GetRoom fetch entirely for bypass-eligible senders (zero added cost for owners, admins, bots, and thread replies)
  • Add inline isBot helper (duplicates room-service/helper.go convention; future cleanup to shared pkg/botid acknowledged)

Tests

  • Extend handler test table with cases covering: owner/admin/bot fast-paths (no GetRoom call), member rejection in large rooms, boundary conditions (count == threshold allowed, count > threshold rejected), and thread reply exemption
  • Add unit tests for canBypassLargeRoomCap, isBot, and marshalErrorReply dispatch logic
  • Regenerate mocks via make generate SERVICE=message-gatekeeper

Implementation Details

  • Cost optimization: Bypass check runs before GetRoom lookup, so owners, admins, bots, and thread replies pay zero added Mongo cost
  • Error classification: GetRoom failures trigger infrastructure error (NACK + redelivery); large-room rejection is a validation error (ACK + coded reply)
  • Backward compatibility: omitempty on Code field ensures existing error reply consumers see identical wire format
  • Boundary: Rejection uses strict greater-than (userCount > threshold), so rooms at exactly the threshold are allowed

https://claude.ai/code/session_017HzARgaSEatexstYkuvnnw

Summary by CodeRabbit

  • New Features
    • Enforced large-room post restriction for top-level messages in rooms over a configurable threshold (env var, default 500); thread replies bypass; owners, admins, and recognized bot accounts may bypass.
  • Error Responses
    • Rejection payloads can include a machine-readable error code for clearer handling.
  • Documentation
    • Added design and rollout plan detailing behavior and config.
  • Tests
    • Expanded unit tests for boundaries, bypass rules, error shapes, and failure classifications.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR enforces a large-room post restriction in message-gatekeeper: top-level (non-thread) messages are rejected when the room's user count exceeds a configurable threshold (default 500) unless sent by an owner, admin, or bot. Implementation includes coded error replies, room-lookup capability via a new store interface, bypass predicates, and comprehensive test coverage.

Changes

Large-Room Post Restriction Feature

Layer / File(s) Summary
Wire / Model Contracts
pkg/model/error.go, pkg/model/model_test.go, pkg/model/subscription.go, pkg/natsutil/reply.go, pkg/natsutil/reply_test.go
ErrorResponse adds optional Code field with omitempty. New MarshalErrorWithCode serializes coded errors. RoleAdmin documented. Round-trip JSON tests validate code field presence/absence.
Store Interface & Data Layer
message-gatekeeper/store.go, message-gatekeeper/store_mongo.go, message-gatekeeper/mock_store_test.go
Store interface expands with GetRoomUserCount(ctx, roomID) method. codedError type and errLargeRoomPostRestricted sentinel added. MongoStore initializes rooms collection and implements GetRoomUserCount with contextual error wrapping. Mock regenerated.
History-Service mongorepo
history-service/internal/mongorepo/room.go, history-service/internal/service/service.go
Switches RoomRepo to use mongoutil.Collection[model.Room], initializes collection via mongoutil.NewCollection, and updates projection helper imports.
Helper Functions
message-gatekeeper/helper.go
Bot detection via compiled pattern and isBot(account) helper (suffix .bot or prefix p_).
Handler Configuration Wiring
message-gatekeeper/main.go, message-gatekeeper/handler.go
Adds LargeRoomThreshold config (env LARGE_ROOM_THRESHOLD, default 500). NewHandler accepts and stores largeRoomThreshold. HandleJetStreamMsg uses handler marshalErrorReply for error responses.
Handler Large-Room Logic
message-gatekeeper/handler.go
processMessage enforces restriction: for non-thread top-level sends, checks exemption via canBypassLargeRoomCap; if not exempt, fetches room user count and rejects with errLargeRoomPostRestricted when userCount > largeRoomThreshold.
Tests & Boundary Validation
message-gatekeeper/handler_test.go, pkg/natsutil/reply_test.go, pkg/model/model_test.go
TestHandler_ProcessMessage expanded with per-case threshold, checkErr, and wantNoPublish. New cases: bypass fast-paths for owner/admin/bot; boundary tests (equal allowed, > rejected); thread replies bypass check; GetRoomUserCount failures classified as infra errors (including mongo.ErrNoDocuments). New unit tests: TestIsBot, TestCanBypassLargeRoomCap. Marshal helper tests added/updated.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • hmchangw/chat#41: Modifies history-service RoomRepo initialization to use mongoutil.NewCollection, overlapping the same mongorepo change.

Suggested reviewers

  • mliu33

Poem

🐰 A rabbit hops past the user-count gate,
Admins and bots skip the tally's weight,
Five hundred stands, the rule is plain,
Coded replies explain the refrain.
Threads slip through — the rest abstain.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: implementing a large-room post restriction feature in the message-gatekeeper service. The title is specific, accurate, and reflects the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/validate-message-sending-5HTd9

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
message-gatekeeper/handler.go (1)

54-63: 💤 Low value

Constructor argument list is starting to grow — consider an options struct for future maintainability.

NewHandler now takes six positional args (store, publish, reply, siteID, parentFetcher, largeRoomThreshold). Each subsequent gate added to the gatekeeper will further bloat this signature. A HandlerOptions (or Config) struct passed by value/pointer would reduce call-site noise and prevent silent positional misordering of same-typed args (e.g., another int config in the future). Not a blocker for this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@message-gatekeeper/handler.go` around lines 54 - 63, The NewHandler
constructor has too many positional arguments; create a new HandlerOptions (or
Config) struct containing fields store, publish, reply, siteID, parentFetcher,
and largeRoomThreshold, then change NewHandler to accept a single HandlerOptions
(pointer or value) and use its fields to populate and return the Handler; update
all call sites to construct and pass the options struct to avoid positional
misordering and make future additions non-breaking.
message-gatekeeper/store.go (1)

30-36: 💤 Low value

Consider broadening the user-facing message to reflect bypass policy.

The sentinel's Message says "only owners can post in this room", but the bypass list now also includes admins and bots. End-users hitting this error in the future may be confused if/when the role-update RPC starts allowing admin assignments. A phrasing like "only owners and admins can post in this large room" would stay aligned with the comment on lines 30–32 without leaking implementation details (bot pattern).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@message-gatekeeper/store.go` around lines 30 - 36, The user-facing Message in
errLargeRoomPostRestricted (a codedError with Code codeLargeRoomPostRestricted)
is too specific ("only owners can post...") and should be broadened to match the
bypass policy described in the comment; update the Message string in
errLargeRoomPostRestricted to a neutral, accurate phrase such as "only owners
and admins can post in this large room" (leave the variable name
errLargeRoomPostRestricted and the Code field unchanged).
message-gatekeeper/handler_test.go (1)

326-400: ⚡ Quick win

Fast-path success cases don't assert canonical publish actually happens.

The owner/admin/bot bypass cases set wantErr: false and capture published, but never assert the slice is non-empty (no checkResult). A regression where the bypass path fell through to a code path that returns nil error without publishing would slip past these tests. Mirroring the existing happy-path checks (assert.Len(t, published, 1)) on the bypass cases would close that gap.

♻️ Example tightening for the owner case (apply to admin/bot/thread-reply analogously)
 			setupPub: func() (publishFunc, *[]publishedMsg) {
 				var published []publishedMsg
 				return makePublishFunc(&published, nil), &published
 			},
 			wantErr: false,
+			checkResult: func(t *testing.T, data []byte, published []publishedMsg) {
+				require.NotNil(t, data)
+				assert.Len(t, published, 1, "bypass path must still publish to MESSAGES_CANONICAL")
+			},
 		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@message-gatekeeper/handler_test.go` around lines 326 - 400, The three
fast-path tests ("owner sends in big room — fast-path skips GetRoom", "admin
sends in big room — fast-path skips GetRoom", "bot account in big room with
member role — fast-path skips GetRoom") currently set wantErr: false and collect
published via makePublishFunc but never assert that publishing actually
occurred; update each case to assert the publish happened (e.g. call the
existing check/assert helper used elsewhere or add assert.Len(t, *publishedPtr,
1)) after the handler runs so the test verifies published (the published slice
returned by setupPub/makePublishFunc and the publishedMsg type) is non-empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@message-gatekeeper/main.go`:
- Line 33: The env tag for the LargeRoomThreshold config field uses a
non-prefixed name; change the struct tag on LargeRoomThreshold to use a
service-prefixed SCREAMING_SNAKE_CASE env var (e.g.,
MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD) instead of LARGE_ROOM_THRESHOLD, update
the envDefault if needed, and search for any other references to the old env
name to update them accordingly so the service follows the env-var naming
convention.

---

Nitpick comments:
In `@message-gatekeeper/handler_test.go`:
- Around line 326-400: The three fast-path tests ("owner sends in big room —
fast-path skips GetRoom", "admin sends in big room — fast-path skips GetRoom",
"bot account in big room with member role — fast-path skips GetRoom") currently
set wantErr: false and collect published via makePublishFunc but never assert
that publishing actually occurred; update each case to assert the publish
happened (e.g. call the existing check/assert helper used elsewhere or add
assert.Len(t, *publishedPtr, 1)) after the handler runs so the test verifies
published (the published slice returned by setupPub/makePublishFunc and the
publishedMsg type) is non-empty.

In `@message-gatekeeper/handler.go`:
- Around line 54-63: The NewHandler constructor has too many positional
arguments; create a new HandlerOptions (or Config) struct containing fields
store, publish, reply, siteID, parentFetcher, and largeRoomThreshold, then
change NewHandler to accept a single HandlerOptions (pointer or value) and use
its fields to populate and return the Handler; update all call sites to
construct and pass the options struct to avoid positional misordering and make
future additions non-breaking.

In `@message-gatekeeper/store.go`:
- Around line 30-36: The user-facing Message in errLargeRoomPostRestricted (a
codedError with Code codeLargeRoomPostRestricted) is too specific ("only owners
can post...") and should be broadened to match the bypass policy described in
the comment; update the Message string in errLargeRoomPostRestricted to a
neutral, accurate phrase such as "only owners and admins can post in this large
room" (leave the variable name errLargeRoomPostRestricted and the Code field
unchanged).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4639664d-3a21-4909-a554-5a832e33ef9e

📥 Commits

Reviewing files that changed from the base of the PR and between ee87139 and 1d40f93.

📒 Files selected for processing (14)
  • docs/superpowers/plans/2026-05-07-large-room-post-restriction.md
  • docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md
  • message-gatekeeper/handler.go
  • message-gatekeeper/handler_test.go
  • message-gatekeeper/helper.go
  • message-gatekeeper/main.go
  • message-gatekeeper/mock_store_test.go
  • message-gatekeeper/store.go
  • message-gatekeeper/store_mongo.go
  • pkg/model/error.go
  • pkg/model/model_test.go
  • pkg/model/subscription.go
  • pkg/natsutil/reply.go
  • pkg/natsutil/reply_test.go

MongoUsername string `env:"MONGO_USERNAME" envDefault:""`
MongoPassword string `env:"MONGO_PASSWORD" envDefault:""`
MaxWorkers int `env:"MAX_WORKERS" envDefault:"100"`
LargeRoomThreshold int `env:"LARGE_ROOM_THRESHOLD" envDefault:"500"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a service-prefixed env var for the new threshold config.

Line 33 introduces a service-specific variable as LARGE_ROOM_THRESHOLD; this should be prefixed to avoid cross-service collisions and follow the config convention.

🔧 Suggested change
-	LargeRoomThreshold int             `env:"LARGE_ROOM_THRESHOLD" envDefault:"500"`
+	LargeRoomThreshold int             `env:"MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD" envDefault:"500"`

As per coding guidelines: “Environment variable names must be SCREAMING_SNAKE_CASE prefixed with service name for service-specific vars”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LargeRoomThreshold int `env:"LARGE_ROOM_THRESHOLD" envDefault:"500"`
LargeRoomThreshold int `env:"MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD" envDefault:"500"`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@message-gatekeeper/main.go` at line 33, The env tag for the
LargeRoomThreshold config field uses a non-prefixed name; change the struct tag
on LargeRoomThreshold to use a service-prefixed SCREAMING_SNAKE_CASE env var
(e.g., MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD) instead of LARGE_ROOM_THRESHOLD,
update the envDefault if needed, and search for any other references to the old
env name to update them accordingly so the service follows the env-var naming
convention.

@general-lex general-lex force-pushed the claude/validate-message-sending-5HTd9 branch from 1d40f93 to 01c7eaf Compare May 8, 2026 02:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/superpowers/plans/2026-05-07-large-room-post-restriction.md (1)

1308-1312: 💤 Low value

Production comment will be factually wrong — admin and bot bypass paths are omitted.

The comment block inserted into handler.go at Step 7.6 states "only owners may send top-level messages" and "owner sends are exempt", but the actual bypass predicate also covers RoleAdmin and bot accounts. This inaccuracy will mislead future maintainers.

💡 Suggested fix
-// Large-room post restriction: in rooms with more than the configured
-// threshold of members, only owners may send top-level messages. Thread
-// replies are exempt regardless of room size; owner sends are exempt
-// regardless of room size. Both bypasses skip the Room fetch entirely
-// (approach B — owner fast-path).
+// Large-room post restriction: in rooms with more than the configured
+// threshold of members, only owners, admins, and bots may send top-level
+// messages. Thread replies are exempt regardless of room size; bypass-eligible
+// senders (owner/admin/bot) skip the Room fetch entirely (approach B — fast-path).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-07-large-room-post-restriction.md` around
lines 1308 - 1312, The comment describing the "Large-room post restriction" in
handler.go is inaccurate: the bypass predicate also allows RoleAdmin and bot
accounts, not just owners. Update the comment block (the Step 7.6 large-room
post restriction comment) to explicitly list RoleAdmin and bot accounts as
additional bypasses alongside owners, retain that thread replies and owner sends
are exempt, and note that these bypasses skip the Room fetch (owner fast-path)
so the comment matches the actual predicate logic that checks owner, RoleAdmin,
and bot accounts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-07-large-room-post-restriction.md`:
- Around line 1067-1097: The rejection test case "member sends in big room —
rejected with codedError" (and the two other reject cases) is missing a
checkResult that asserts no canonical publish occurred; update each reject case
to add a checkResult function that takes (t *testing.T, resp
*model.SendMessageResponse, publishedPtr *[]publishedMsg) and calls
assert.Empty(t, *publishedPtr) (and keep existing checkErr logic), ensuring you
use the same published variable returned by setupPub/makePublishFunc and the
publishedPtr parameter to verify no messages were published.

In `@docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md`:
- Around line 255-259: The user-facing error for errLargeRoomPostRestricted
currently reads "only owners can post in this room" but the bypass policy also
permits admins and bots; update the Message value on errLargeRoomPostRestricted
to a role-neutral phrase such as "posting is restricted in this room" (or
similar) and propagate the same wording to the implementation in store.go, the
spec's wire-format example, the assertion in the testing strategy table, and the
TestMarshalErrorWithCode test so the error text remains consistent across code,
docs, and tests.

---

Nitpick comments:
In `@docs/superpowers/plans/2026-05-07-large-room-post-restriction.md`:
- Around line 1308-1312: The comment describing the "Large-room post
restriction" in handler.go is inaccurate: the bypass predicate also allows
RoleAdmin and bot accounts, not just owners. Update the comment block (the Step
7.6 large-room post restriction comment) to explicitly list RoleAdmin and bot
accounts as additional bypasses alongside owners, retain that thread replies and
owner sends are exempt, and note that these bypasses skip the Room fetch (owner
fast-path) so the comment matches the actual predicate logic that checks owner,
RoleAdmin, and bot accounts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73ca71c4-43a8-43c4-8d57-602f99c11e06

📥 Commits

Reviewing files that changed from the base of the PR and between 1d40f93 and 01c7eaf.

📒 Files selected for processing (14)
  • docs/superpowers/plans/2026-05-07-large-room-post-restriction.md
  • docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md
  • message-gatekeeper/handler.go
  • message-gatekeeper/handler_test.go
  • message-gatekeeper/helper.go
  • message-gatekeeper/main.go
  • message-gatekeeper/mock_store_test.go
  • message-gatekeeper/store.go
  • message-gatekeeper/store_mongo.go
  • pkg/model/error.go
  • pkg/model/model_test.go
  • pkg/model/subscription.go
  • pkg/natsutil/reply.go
  • pkg/natsutil/reply_test.go
✅ Files skipped from review due to trivial changes (3)
  • message-gatekeeper/helper.go
  • pkg/model/subscription.go
  • pkg/model/error.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • message-gatekeeper/mock_store_test.go
  • message-gatekeeper/main.go
  • message-gatekeeper/store_mongo.go
  • message-gatekeeper/store.go
  • pkg/natsutil/reply.go
  • message-gatekeeper/handler.go
  • message-gatekeeper/handler_test.go

Comment thread docs/superpowers/plans/2026-05-07-large-room-post-restriction.md
Comment on lines +255 to +259
var errLargeRoomPostRestricted = &codedError{
Code: "large_room_post_restricted",
Message: "only owners can post in this room",
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Misleading user-facing error message — admins and bots are also permitted senders.

The Message field says "only owners can post in this room", but the bypass predicate also allows RoleAdmin and bot accounts. While the admin clause is currently dormant, this message will surface to regular members and, once admin assignment is wired in room-service, could confuse an admin who receives a rejection for any other reason. Prefer a role-neutral phrasing that stays accurate as the bypass policy evolves.

💡 Suggested fix
-var errLargeRoomPostRestricted = &codedError{
-	Code:    "large_room_post_restricted",
-	Message: "only owners can post in this room",
-}
+var errLargeRoomPostRestricted = &codedError{
+	Code:    "large_room_post_restricted",
+	Message: "posting is restricted to owners and admins in this room",
+}

The same change should be applied consistently in store.go (the actual implementation), the wire-format example in this spec (line 281), the assertion in the testing strategy table (line 352), and the TestMarshalErrorWithCode test in pkg/natsutil/reply_test.go (line 83).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var errLargeRoomPostRestricted = &codedError{
Code: "large_room_post_restricted",
Message: "only owners can post in this room",
}
```
var errLargeRoomPostRestricted = &codedError{
Code: "large_room_post_restricted",
Message: "posting is restricted to owners and admins in this room",
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md`
around lines 255 - 259, The user-facing error for errLargeRoomPostRestricted
currently reads "only owners can post in this room" but the bypass policy also
permits admins and bots; update the Message value on errLargeRoomPostRestricted
to a role-neutral phrase such as "posting is restricted in this room" (or
similar) and propagate the same wording to the implementation in store.go, the
spec's wire-format example, the assertion in the testing strategy table, and the
TestMarshalErrorWithCode test so the error text remains consistent across code,
docs, and tests.

Copy link
Copy Markdown
Collaborator

@mliu33 mliu33 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, thanks! Just one minor suggestion~

Comment thread message-gatekeeper/store_mongo.go Outdated
// mongo.ErrNoDocuments) is wrapped and returned — the handler treats every
// failure here as an infrastructure error, since reaching this call already
// implies a subscription for the room exists.
func (s *MongoStore) GetRoom(ctx context.Context, roomID string) (*model.Room, error) {
Copy link
Copy Markdown
Collaborator

@mliu33 mliu33 May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the only usage is to get user count, maybe we can update the method name and add projection to return only "UserAcount" field for better performance ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brief on the trade-off, since it's worth a thought:

For: every non-bypass send pays a Mongo round-trip to read Room.UserCount. With SetProjection({"userCount": 1}) we send/receive the _id plus a single int instead of the full ~13-field document. Per call it's a few hundred bytes; on a busy 600-person channel where most senders are regular members, it adds up. The narrower method name (GetRoomUserCount vs. GetRoom) also makes intent obvious — a future maintainer doesn't have to read the call site to know nothing else from Room is consumed.

Against: the other services (room-service, room-worker, broadcast-worker) all have a generic GetRoom that returns the full document, so this method is now slightly inconsistent. And if a future admission rule wants another Room field (Restricted, Type, etc.), we'd either expand the projection or split into a second method.

Net call: the consistency argument is weak when there's no concrete reuse — every service's Store is independent — and YAGNI on the future-rule case (cheaper to widen later than carry weight now). So:

  • Store.GetRoomStore.GetRoomUserCount(ctx, roomID) (int, error)
  • MongoStore decodes into a single-field anonymous struct via the projection
  • All call sites, 16 mock expectations, regenerated mock
  • Spec updated to match (651fd2e)

Generated by Claude Code

general-lex pushed a commit that referenced this pull request May 8, 2026
The cap rule's only consumer of room data is userCount. Narrowing the
Store method to GetRoomUserCount(ctx, roomID) (int, error) and adding a
Mongo projection (`SetProjection({"userCount": 1})`) avoids pulling the
rest of the Room document over the wire on every non-bypass send.

Per-call savings are modest (~250B) but accumulate on the hot path. The
narrower name also signals intent — a future maintainer doesn't have to
read the call site to learn that nothing else from Room is consumed.
Addresses PR #162 review comment from @mliu33.
claude added 20 commits May 8, 2026 07:44
In rooms with more than 500 members, only owners may send top-level
messages. Thread replies are exempt; edits and deletes are unaffected.
Threshold configurable via env var. Single change point in
message-gatekeeper plus a backward-compatible Code field on
model.ErrorResponse.
…tant

Bypass policy now allows owners, admins, and bots (account name matches
\.bot$|^p_) to send in 500+ rooms. RoleAdmin is added to pkg/model as a
constant only — role-update RPC support remains owned by another team.
isBot is duplicated inline in message-gatekeeper rather than shared, to
unblock this PR; promotion to pkg/botid is tracked as future work.
Address findings from the final cross-task code review:
- Add TestHandler_ProcessMessage case for GetRoom returning a wrapped
  mongo.ErrNoDocuments — a distinct sentinel from generic Mongo errors,
  must still classify as infraError per the spec table.
- Add wantNoPublish field on the table struct and assertion in the runner
  so rule-rejection cases prove no canonical publish occurs when blocked.
…stant

The wire code "large_room_post_restricted" was duplicated between the
errLargeRoomPostRestricted sentinel and the slog "reason" field. Pull it
into a single named constant so the error and the log line can never
drift apart.
Address findings from the final code review:
- RoleAdmin in pkg/model/subscription.go now carries an inline comment
  flagging that the constant is recognized by message-gatekeeper's bypass
  but not yet assignable via room-service's role-update RPC.
- errLargeRoomPostRestricted's doc comment now correctly names all three
  bypass categories (owner, admin, bot) instead of just "non-owner".
- Broaden user-facing rejection message to "posting is restricted to
  owners and admins in this room" — admins are bypass-eligible too, so
  saying "only owners" is misleading.
- Assert canonical publish actually fires on owner/admin/bot/thread-reply
  bypass paths so a regression that silently drops a bypassed message
  would fail the test.
The cap rule's only consumer of room data is userCount. Narrowing the
Store method to GetRoomUserCount(ctx, roomID) (int, error) and adding a
Mongo projection (`SetProjection({"userCount": 1})`) avoids pulling the
rest of the Room document over the wire on every non-bypass send.

Per-call savings are modest (~250B) but accumulate on the hot path. The
narrower name also signals intent — a future maintainer doesn't have to
read the call site to learn that nothing else from Room is consumed.
Addresses PR #162 review comment from @mliu33.
PR #161 (1807b75) introduced two undefined-symbol build breaks:
  - mongorepo/room.go used Collection / NewCollection / WithProjection
    without importing pkg/mongoutil (the sibling subscription.go and
    threadroom.go do so correctly with a "mongoutil." prefix).
  - service/service.go's compile-time interface assertion referenced
    mongorepo.RoomRepo without importing the mongorepo package.

Both go build and golangci-lint fail on origin/main HEAD because of
this. Without the fix every PR that merges with main inherits the
break — including this one. Adding the imports + qualifiers locally
unblocks our PR's CI and makes the merge clean.
@general-lex general-lex force-pushed the claude/validate-message-sending-5HTd9 branch from 26ea5a9 to e34b362 Compare May 8, 2026 07:47
…shipped

- Predicate sample uses GetRoomUserCount(int) and the
  codeLargeRoomPostRestricted constant.
- Store interface section reflects the renamed method and the
  SetProjection({"userCount":1}) optimization.
- Error sentinel and wire-format examples show the broadened "posting
  is restricted to owners and admins in this room" message.
- Logging section renames the constant reference and the GetRoom
  failure note.
- Testing strategy table renames the mock-expectation column,
  documents the wantNoPublish field, and the bypass-case
  assert.Len(t, published, 1) assertion.

Plan doc is intentionally left as-is — it's a one-shot implementation
checklist whose value as a historical record lives in the git log.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
message-gatekeeper/main.go (1)

33-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a service-prefixed env var for threshold config.

Line 33 still uses LARGE_ROOM_THRESHOLD; this is service-specific and should be prefixed (for example: MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD) to avoid cross-service collisions.

Suggested patch
-	LargeRoomThreshold int             `env:"LARGE_ROOM_THRESHOLD" envDefault:"500"`
+	LargeRoomThreshold int             `env:"MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD" envDefault:"500"`

As per coding guidelines: “Environment variable names must be SCREAMING_SNAKE_CASE prefixed with service name for service-specific vars”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@message-gatekeeper/main.go` at line 33, The env tag for the
LargeRoomThreshold config field uses a generic name `LARGE_ROOM_THRESHOLD`;
change it to a service-prefixed env var (e.g.
`MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD`) in the struct tag for
LargeRoomThreshold so the env loader reads the prefixed variable; update any
config loading, documentation, and tests that reference the old
`LARGE_ROOM_THRESHOLD` name to the new `MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD`
name to avoid cross-service collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-07-large-room-post-restriction.md`:
- Around line 183-200: Update the plan text to match the shipped API: replace
all references to GetRoom with GetRoomUserCount and show the correct signature
GetRoomUserCount(ctx context.Context, roomID string) (int, error); update the
example Store interface block and any checklist items/step descriptions that
mention GetRoom (including "Add `GetRoom` to the `Store` interface") so they
reference Store and GetRoomUserCount instead, and adjust the note about
regenerating mocks to reflect the actual method name so contributors implement
against the current API.

In `@docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md`:
- Around line 171-173: Update the error-classification text to reference the
current method name: replace mentions of GetRoom with GetRoomUserCount so it
accurately describes where `*infraError` (NACK + JetStream redelivery) applies;
keep the rest of the sentence intact (i.e., plain validation error
`errLargeRoomPostRestricted` (ACK + reply) for non-owner-in-big-room) to mirror
how `GetSubscription` errors are classified.
- Around line 37-47: The fenced code block that shows the pipeline diagram
starting with "Subject parse → SiteID match → unmarshal → ID/content validation"
is missing a language tag and triggers MD040; add an appropriate language
identifier (for example "text") to the opening fence (i.e., change ``` to
```text) so the block is lintable while keeping the diagram content unchanged.

---

Duplicate comments:
In `@message-gatekeeper/main.go`:
- Line 33: The env tag for the LargeRoomThreshold config field uses a generic
name `LARGE_ROOM_THRESHOLD`; change it to a service-prefixed env var (e.g.
`MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD`) in the struct tag for
LargeRoomThreshold so the env loader reads the prefixed variable; update any
config loading, documentation, and tests that reference the old
`LARGE_ROOM_THRESHOLD` name to the new `MESSAGE_GATEKEEPER_LARGE_ROOM_THRESHOLD`
name to avoid cross-service collisions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4dbf9df0-b7f8-4a0a-a621-caa2cff43f88

📥 Commits

Reviewing files that changed from the base of the PR and between 26ea5a9 and 651fd2e.

📒 Files selected for processing (16)
  • docs/superpowers/plans/2026-05-07-large-room-post-restriction.md
  • docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md
  • history-service/internal/mongorepo/room.go
  • history-service/internal/service/service.go
  • message-gatekeeper/handler.go
  • message-gatekeeper/handler_test.go
  • message-gatekeeper/helper.go
  • message-gatekeeper/main.go
  • message-gatekeeper/mock_store_test.go
  • message-gatekeeper/store.go
  • message-gatekeeper/store_mongo.go
  • pkg/model/error.go
  • pkg/model/model_test.go
  • pkg/model/subscription.go
  • pkg/natsutil/reply.go
  • pkg/natsutil/reply_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/model/error.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/model/model_test.go
  • message-gatekeeper/mock_store_test.go
  • message-gatekeeper/helper.go
  • message-gatekeeper/handler.go
  • message-gatekeeper/store.go
  • message-gatekeeper/handler_test.go
  • pkg/natsutil/reply.go

Comment on lines +183 to +200
## Task 3: Add `GetRoom` to gatekeeper `Store` + `MongoStore` impl + regen mocks

**Files:**
- Modify: `message-gatekeeper/store.go`
- Modify: `message-gatekeeper/store_mongo.go`
- Regenerate: `message-gatekeeper/mock_store_test.go`

(No new test for `GetRoom` itself — `MongoStore` implementations in this repo are exercised through handler tests with mocked stores, and the spec explicitly omits integration tests for this rule.)

- [ ] **Step 3.1: Add `GetRoom` to the `Store` interface**

In `message-gatekeeper/store.go`, replace the existing `Store` interface block with:

```go
type Store interface {
GetSubscription(ctx context.Context, account, roomID string) (*model.Subscription, error)
GetRoom(ctx context.Context, roomID string) (*model.Room, error)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Plan steps still reference the old GetRoom API.

This section instructs implementing GetRoom(ctx, roomID) (*model.Room, error), but the finalized change uses GetRoomUserCount(ctx, roomID) (int, error). Please align the plan text to the shipped API so future contributors don’t implement against stale instructions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-07-large-room-post-restriction.md` around
lines 183 - 200, Update the plan text to match the shipped API: replace all
references to GetRoom with GetRoomUserCount and show the correct signature
GetRoomUserCount(ctx context.Context, roomID string) (int, error); update the
example Store interface block and any checklist items/step descriptions that
mention GetRoom (including "Add `GetRoom` to the `Store` interface") so they
reference Store and GetRoomUserCount instead, and adjust the note about
regenerating mocks to reflect the actual method name so contributors implement
against the current API.

Comment on lines +37 to +47
```
Subject parse → SiteID match → unmarshal → ID/content validation
GetSubscription (existing)
[NEW] LARGE-ROOM CHECK
resolveQuoteSnapshot (existing)
Build Message + publish to MESSAGES_CANONICAL
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to fenced code block.

The code fence starting at Line 37 is missing a language identifier, which triggers markdown lint (MD040).

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 37-37: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md`
around lines 37 - 47, The fenced code block that shows the pipeline diagram
starting with "Subject parse → SiteID match → unmarshal → ID/content validation"
is missing a language tag and triggers MD040; add an appropriate language
identifier (for example "text") to the opening fence (i.e., change ``` to
```text) so the block is lintable while keeping the diagram content unchanged.

Comment on lines +171 to +173
`*infraError` (NACK + JetStream redelivery) for `GetRoom` failures. Plain
validation error (`errLargeRoomPostRestricted`, ACK + reply) for
non-owner-in-big-room. Mirrors how `GetSubscription` errors are classified
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the updated method name in error-classification text.

Line 171 references GetRoom failures, but the implementation now enforces this via GetRoomUserCount. Updating the term here avoids confusion during debugging and test maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-07-large-room-post-restriction-design.md`
around lines 171 - 173, Update the error-classification text to reference the
current method name: replace mentions of GetRoom with GetRoomUserCount so it
accurately describes where `*infraError` (NACK + JetStream redelivery) applies;
keep the rest of the sentence intact (i.e., plain validation error
`errLargeRoomPostRestricted` (ACK + reply) for non-owner-in-big-room) to mirror
how `GetSubscription` errors are classified.

@mliu33 mliu33 merged commit 127d3e0 into main May 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants